54 Pandas模块的统计计算
本章定位(复习与补充训练层):本章对应正课第6章《NumPy 数组运算》(章节 6)中的统计运算部分,并与第33章《数据分布情况分析》(章节 33)互为补充,用于复习、补缺与额外练习。建议先不看讲解,直接尝试下方平台任务,再对照解析补弱项。本章不属于必修主线。先做本章『动手与思考』第 1 题与平台任务自测,通过即可跳过本章。
54.1 引言统计计算是数据分析的核心
统计计算帮助我们量化风险(波动率、VaR)、评估收益、发现规律(相关性、趋势、周期性)并支持决策;Pandas 提供丰富的统计计算方法,结合 NumPy 的向量化运算,可以高效处理大规模金融数据。
54.2 本章学习目标
通过本章的复习与补充训练,你将能够:
- 用
describe()与mean()/std()/median()/quantile()等方法完成描述性统计,并说明 Pandas 与 NumPy 在方差口径(ddof)上的默认差异 - 计算并解读偏度与峰度,据此判断收益分布的对称性与厚尾程度
- 用
cumsum()/cumprod()计算累积统计量,用rolling()与expanding()窗口计算移动平均、滚动波动率等指标 - 用
groupby()完成分组聚合(含agg与自定义函数),用corr()/cov()度量多资产间的相关结构,独立完成平台任务
54.3 描述性统计
54.3.1 数学原理(描述性统计)
给定数据集 \(X = \{x_1, x_2, \ldots, x_n\}\):
集中趋势:
- 均值(Mean): \(\bar{x} = \frac{1}{n}\sum_{i=1}^n x_i\)
- 中位数(Median): 排序后位于中间位置的值
- 众数(Mode): 出现频率最高的值
离散程度:
- 样本方差(Sample Variance): \(s^2 = \frac{1}{n-1}\sum_{i=1}^n (x_i - \bar{x})^2\)(Pandas 的
var()默认 ddof=1,即样本方差;NumPy 默认 ddof=0,为总体方差 \(\sigma^2 = \frac{1}{n}\sum_{i=1}^n (x_i - \bar{x})^2\)) - 标准差(Standard Deviation): \(s = \sqrt{s^2}\)(Pandas 的
std()同样默认样本口径 ddof=1) - 极差(Range): \(\max(X) - \min(X)\)
分布形状:
- 偏度(Skewness): 衡量分布的对称性
- 峰度(Kurtosis): 衡量分布的尖峰/厚尾程度
54.3.2 基础统计量计算
任务要求(归纳自块内任务注释与代码,同一代码块含四问):
- 任务一:从给定地址读取 QDII 基金净值 Excel(117 行 × 5 列),把”日期”列转为 datetime 并设为索引,删除缺失行,以首个交易日净值归一化后绘制四条净值曲线(带网格),保存为
1.png - 任务二:输出各基金净值的最大值、最小值,以及两者所在的日期索引(
idxmax/idxmin) - 任务三:用
diff()计算每日净值变动金额,输出前 5 行与后 5 行 - 任务四:用
pct_change()计算每日净值百分比变动(该段head()/tail()未加print,控制台不显示),再用任务三的差分除以shift(1)的前值重建百分比变动并输出前 5 行与后 5 行
请将代码原样输入教学平台(注释除外),判定以平台为准。
平台任务(平台原始代码)
以下代码与教学平台任务要求完全一致:
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
#任务一
import pandas as pd
import matplotlib.pyplot as plt # 导入Matplotlib绘图库
# 从Excel文件读取数据存入value_QDII
value_QDII = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/1726648479386.xlsx")
value_QDII["日期"] = pd.to_datetime(value_QDII["日期"] , format='%Y%m%d') # 转换为日期时间格式
value_QDII.set_index("日期",inplace=True) # 将日期列设为value_QDII数据框的索引
value_QDII = value_QDII.dropna() #删除缺失值所在行
(value_QDII/value_QDII.iloc[0]).plot(figsize=(8,6),grid=True) #将基金净值按首个交易日进行归一处理并可视化
plt.savefig("1.png") # 保存图形至文件
#任务二
import pandas as pd
# 从Excel文件读取数据存入value_QDII
value_QDII = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/1726648479386.xlsx")
value_QDII["日期"] = pd.to_datetime(value_QDII["日期"] , format='%Y%m%d') # 转换为日期时间格式
value_QDII.set_index("日期",inplace=True) # 将日期列设为value_QDII数据框的索引
print(value_QDII.max()) #找出每只基金净值的最大值
print(value_QDII.min()) #找出每只基金净值的最小值
print(value_QDII.idxmax()) #最大值所在的索引值
print(value_QDII.idxmin()) #最小值所在的索引值
#任务三
import pandas as pd
# 从Excel文件读取数据存入value_QDII
value_QDII = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/1726648479386.xlsx")
value_QDII["日期"] = pd.to_datetime(value_QDII["日期"] , format='%Y%m%d') # 转换为日期时间格式
value_QDII.set_index("日期",inplace=True) # 将日期列设为value_QDII数据框的索引
value_QDII_diff = value_QDII.diff() # 计算基金每日净值的变动金额
print(value_QDII_diff.head()) #查看前五行数据
print(value_QDII_diff.tail()) #查看后五行数据
#任务四
import pandas as pd
# 从Excel文件读取数据存入value_QDII
value_QDII = pd.read_excel("https://huoran.oss-cn-shenzhen.aliyuncs.com/1726648479386.xlsx")
value_QDII_pctchangel = value_QDII.pct_change() #直接使用函数pct_change计算基金每日净值百分比变动
value_QDII_pctchangel.head() # 查看value_QDII_pctchangel前5行数据
value_QDII_pctchangel.tail() # 查看value_QDII_pctchangel后5行数据
value_QDII_diff = value_QDII.diff() # 计算差分值
value_QDII_pctchange2 = value_QDII_diff/value_QDII.shift(1) #运用任务三的结果计算基金每日净值百分比变动
print(value_QDII_pctchange2.head()) # 输出前几行数据
print(value_QDII_pctchange2.tail()) # 输出最后几行数据预期输出(数据来自教学平台的 OSS 直链,OSS 直链本机实测;具体数值以平台运行结果为准):
任务二先输出四组统计量(依次为各基金最大值、最小值、最大值所在日期、最小值所在日期),作者实测如下:
华夏全球股票 1.1114
华安香港精选股票 1.8300
工银瑞信全球股票 2.3920
易方达亚洲精选股票 1.0890
dtype: float64
华夏全球股票 0.9131
华安香港精选股票 1.4310
工银瑞信全球股票 2.1320
易方达亚洲精选股票 0.8460
dtype: float64
华夏全球股票 2024-06-19
华安香港精选股票 2024-06-20
工银瑞信全球股票 2024-05-20
易方达亚洲精选股票 2024-06-19
dtype: datetime64[ns]
华夏全球股票 2024-01-04
华安香港精选股票 2024-01-22
工银瑞信全球股票 2024-01-17
易方达亚洲精选股票 2024-01-17
dtype: datetime64[ns]
任务三随后输出差分表的前 5 行(首行全为 NaN)与后 5 行,行索引为日期,如 2024-01-05 行的四只基金变动为 0.0004、-0.011、-0.004、-0.006。任务四输出的表结构不同:因该段未 set_index,表中多出一个整数索引与一个”日期”列(取值形如 4.94e-08,是 yyyymmdd 整数日期做差分再除以前值的结果),首行同样全为 NaN——这是任务三、任务四代码差异的直接体现。任务一仅生成归一化净值曲线图 1.png,无控制台输出。
import pandas as pd
import numpy as np
# 创建股票收益率数据
np.random.seed(42)
returns_data = {
'贵州茅台': np.random.normal(0.001, 0.02, 100), # 日收益率
'五粮液': np.random.normal(0.0008, 0.025, 100),
'招商银行': np.random.normal(0.0005, 0.015, 100),
'中国平安': np.random.normal(0.0006, 0.018, 100)
}
df_returns = pd.DataFrame(returns_data)
print('收益率数据(前10行):')
print(df_returns.head(10))
# 计算各项统计量
print('\n基础统计量:')
print(f'均值:\n{df_returns.mean()}')
print(f'\n中位数:\n{df_returns.median()}')
print(f'\n标准差:\n{df_returns.std()}')
print(f'\n方差:\n{df_returns.var()}')
print(f'\n最小值:\n{df_returns.min()}')
print(f'\n最大值:\n{df_returns.max()}')
# 一次性获取所有描述性统计
print('\n完整描述性统计:')
desc_stats = df_returns.describe()
print(desc_stats)54.3.3 describe()方法详解
describe() 方法返回的统计量:
| 统计量 | 含义 | 公式 |
|---|---|---|
| count | 非缺失值数量 | \(n_{\text{valid}}\) |
| mean | 均值 | \(\bar{x} = \frac{1}{n}\sum x_i\) |
| std | 标准差 | \(\sqrt{\frac{1}{n-1}\sum(x_i-\bar{x})^2}\) |
| min | 最小值 | \(\min(X)\) |
| 25% | 第一四分位数 | \(Q_1 = P_{25}\) |
| 50% | 第二四分位数(中位数) | \(Q_2 = P_{50}\) |
| 75% | 第三四分位数 | \(Q_3 = P_{75}\) |
| max | 最大值 | \(\max(X)\) |
54.3.4 分位数计算
# 计算常用分位数
quantiles = [0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99]
print('自定义分位数:')
print(df_returns.quantile(quantiles))
# 四分位距(IQR)
Q1 = df_returns.quantile(0.25)
Q3 = df_returns.quantile(0.75)
IQR = Q3 - Q1
print('\n四分位距(IQR):')
print(IQR)
# 识别异常值(超出1.5*IQR)
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
print('\n异常值边界:')
print(f'下界:\n{lower_bound}')
print(f'\n上界:\n{upper_bound}')
# 检测异常值
outliers = (df_returns < lower_bound) | (df_returns > upper_bound)
print(f'\n异常值数量:')
print(outliers.sum())异常值检测的数学原理:
箱线图规则(Boxplot Rule):
- 正常值: \([Q_1 - 1.5 \times IQR, Q_3 + 1.5 \times IQR]\)
- 温和异常值(Mild Outlier): 距离箱体 \(1.5-3 \times IQR\)
- 极端异常值(Extreme Outlier): 距离箱体 \(> 3 \times IQR\)
Z-Score方法:
\[ Z_i = \frac{x_i - \bar{x}}{\sigma} \] 通常认为 \(|Z| > 3\) 为异常值。
54.4 偏度与峰度
54.4.1 数学原理(随机性与波动)
偏度(Skewness):
\[ \gamma_1 = \frac{E[(X-\mu)^3]}{\sigma^3} = \frac{\frac{1}{n}\sum_{i=1}^n (x_i-\bar{x})^3}{\left[\sqrt{\frac{1}{n}\sum_{i=1}^n (x_i-\bar{x})^2}\right]^3} \]
峰度(Kurtosis):
\[ \gamma_2 = \frac{E[(X-\mu)^4]}{\sigma^4} - 3 = \frac{\frac{1}{n}\sum_{i=1}^n (x_i-\bar{x})^4}{\left[\frac{1}{n}\sum_{i=1}^n (x_i-\bar{x})^2\right]^2} - 3 \]
54.4.2 金融意义
# 计算偏度和峰度
skewness = df_returns.skew()
kurtosis = df_returns.kurtosis()
print('偏度(Skewness):')
print(skewness)
print('\n峰度(Kurtosis):')
print(kurtosis)
# 解释
print('\n解释:')
for stock in df_returns.columns:
skew_val = skewness[stock]
kurt_val = kurtosis[stock]
# 偏度解释
if skew_val > 0.5:
skew_interp = '右偏(正偏),有较长右尾,极端正收益更多'
elif skew_val < -0.5:
skew_interp = '左偏(负偏),有较长左尾,极端负收益更多'
else:
skew_interp = '近似对称'
# 峰度解释
if kurt_val > 1:
kurt_interp = '尖峰分布,有较多极端值(厚尾)'
elif kurt_val < -1:
kurt_interp = '低峰分布,较为平坦'
else:
kurt_interp = '接近正态分布'
print(f'\n{stock}:')
print(f' 偏度={skew_val:.3f} → {skew_interp}')
print(f' 峰度={kurt_val:.3f} → {kurt_interp}')金融应用:
- 正偏度:
- 大多数时间小幅下跌
- 偶尔出现大幅上涨(如牛市中的股票)
- 符合投资者偏好(有限亏损,无限收益)
- 负偏度:
- 大多数时间小幅上涨
- 偶尔出现暴跌(如高杠杆资产)
- 风险较高(黑天鹅事件)
- 高峰度:
- 厚尾(Fat Tails): 极端事件发生的概率高于正态分布
- 金融市场的典型特征
- 风险管理需要考虑极端情况
正态分布检验:
如果数据完全服从正态分布:
- 偏度 ≈ 0
- 峰度 ≈ 0(超额峰度)
- Jarque-Bera检验: \(JB = \frac{n}{6}\left(\gamma_1^2 + \frac{\gamma_2^2}{4}\right)\)
54.5 累积统计量
# 创建价格序列
prices = pd.DataFrame({
'贵州茅台': [1850, 1860, 1855, 1870, 1865, 1880],
'五粮液': [220, 218, 222, 225, 223, 226]
})
print('原始价格:')
print(prices)
# 累积和
print('\n累积和:')
print(prices.cumsum())
# 累积积
print('\n累积积:')
print(prices.cumprod())
# 累积最大值
print('\n累积最大值( expanding maximum):')
print(prices.cummax())
# 累积最小值
print('\n累积最小值( expanding minimum):')
print(prices.cummin())
# 金融应用:累计收益率
initial_prices = prices.iloc[0]
cum_returns = (prices / initial_prices - 1) * 100
print('\n累计收益率(%):')
print(cum_returns)累积统计量的金融应用:
- cumsum: 累计收益、累计成交量
- cumprod: 复利增长(价格相对变化)
- cummax: 回撤分析(Drawdown)
- cummin: 历史最低价监控
54.5.1 回撤计算
# 模拟净值曲线
np.random.seed(42)
nav = pd.DataFrame({
'日期': pd.date_range('2024-01-01', periods=100),
'净值': 1.0 + np.cumsum(np.random.normal(0.001, 0.02, 100))
})
# 计算历史最高点
nav['历史最高'] = nav['净值'].cummax()
# 计算回撤
nav['回撤'] = (nav['净值'] - nav['历史最高']) / nav['历史最高']
print('净值与回撤:')
print(nav.head(20))
# 最大回撤
max_drawdown = nav['回撤'].min()
print(f'\n最大回撤: {max_drawdown:.2%}')
# 可视化
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['Source Han Serif SC', 'SimHei'] # 规范字体在前,SimHei 仅作后备
plt.rcParams['axes.unicode_minus'] = False
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True)
# 净值曲线
ax1.plot(nav['日期'], nav['净值'], label='净值', linewidth=2)
ax1.plot(nav['日期'], nav['历史最高'], label='历史最高', linewidth=2, linestyle='--')
ax1.set_ylabel('净值')
ax1.set_title('净值曲线与回撤分析')
ax1.legend()
ax1.grid(True, alpha=0.3)
# 回撤曲线
ax2.fill_between(nav['日期'], nav['回撤'], 0, alpha=0.3, color='red')
ax2.plot(nav['日期'], nav['回撤'], color='red', linewidth=2)
ax2.set_ylabel('回撤率')
ax2.set_xlabel('日期')
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()回撤的金融意义:
回撤(Drawdown):从历史最高点到当前点的下降幅度
\[ \text{Drawdown}_t = \frac{P_t - \max_{i \leq t} P_i}{\max_{i \leq t} P_i} \]
最大回撤(Maximum Drawdown, MDD):
\[ \text{MDD} = \min_t \text{Drawdown}_t \]
最大回撤是衡量投资策略风险的关键指标:
- MDD = -20%: 意味着历史上曾经从高点下跌20%
- 心理影响: 投资者需要承受20%的亏损
- 恢复要求: 下跌20%需要上涨25%才能回本
54.6 窗口统计量滚动计算
54.6.1 数学原理(滚动窗口)
滚动窗口(Rolling Window):对于时间序列 \(\{x_t\}_{t=1}^T\),窗口大小为 \(w\):
\[ \text{RollingMean}_t = \frac{1}{w}\sum_{i=t-w+1}^t x_i \]
54.6.2 滚动统计量计算
# 创建价格数据
dates = pd.date_range('2024-01-01', periods=100)
np.random.seed(42) # 固定随机种子,保证每次运行结果可复现
prices_ts = pd.DataFrame({
'日期': dates,
'收盘价': 100 + np.cumsum(np.random.normal(0.5, 2, 100))
})
prices_ts = prices_ts.set_index('日期')
print('原始价格:')
print(prices_ts.tail(10))
# 5日滚动均值
prices_ts['MA5'] = prices_ts['收盘价'].rolling(window=5).mean()
# 20日滚动均值
prices_ts['MA20'] = prices_ts['收盘价'].rolling(window=20).mean()
# 5日滚动标准差
prices_ts['STD5'] = prices_ts['收盘价'].rolling(window=5).std()
# 5日滚动最大值
prices_ts['MAX5'] = prices_ts['收盘价'].rolling(window=5).max()
print('\n滚动统计量:')
print(prices_ts.tail(10))
# 计算布林带
prices_ts['布林带_上'] = prices_ts['MA20'] + 2 * prices_ts['收盘价'].rolling(window=20).std()
prices_ts['布林带_下'] = prices_ts['MA20'] - 2 * prices_ts['收盘价'].rolling(window=20).std()
print('\n布林带:')
print(prices_ts[['收盘价', 'MA20', '布林带_上', '布林带_下']].tail(10))金融技术指标:
- 移动平均(Moving Average, MA):平滑价格波动,识别趋势
- 布林带(Bollinger Bands):
- 中轨: \(MA_{20}\)
- 上轨: \(MA_{20} + 2\sigma\)
- 下轨: \(MA_{20} - 2\sigma\)
- 交易信号:价格触及上轨可能超买,触及下轨可能超卖
54.7 expanding窗口
# expanding:从起点到当前点的累积计算
prices_ts['累积均值'] = prices_ts['收盘价'].expanding().mean()
prices_ts['累积标准差'] = prices_ts['收盘价'].expanding().std()
prices_ts['累积最大值'] = prices_ts['收盘价'].expanding().max()
print('扩展窗口统计:')
print(prices_ts[['收盘价', '累积均值', '累积标准差', '累积最大值']].tail(10))
# 金融应用:累计波动率
prices_ts['累计波动率'] = prices_ts['收盘价'].pct_change().expanding().std() * np.sqrt(252)
print('\n年化累计波动率:')
print(prices_ts['累计波动率'].tail(10))rolling vs expanding:
| 特性 | rolling | expanding |
|---|---|---|
| 窗口大小 | 固定 | 不断增长 |
| 计算范围 | \([t-w+1, t]\) | \([1, t]\) |
| 权重 | 等权重 | 等权重 |
| 应用 | 移动平均、短期波动 | 累计收益、长期风险 |
54.8 分组统计 groupby
54.8.1 数学原理(分组聚合)
分组聚合(GroupBy Aggregation):
\[ \text{GroupBy}(K, f, X) = \{(k, f(\{x | key(x) = k\})) | k \in K\} \]
其中:
- \(K\): 键集合
- \(f\): 聚合函数
- \(X\): 数据集
54.8.2 基础分组操作
# 创建行业数据
industry_data = pd.DataFrame({
'股票代码': ['600519.SH', '000858.SZ', '600036.SH', '601318.SH', '000001.SZ', '601398.SH'],
'股票名称': ['贵州茅台', '五粮液', '招商银行', '中国平安', '平安银行', '工商银行'],
'行业': ['白酒', '白酒', '银行', '保险', '银行', '银行'],
'市盈率': [45.2, 35.8, 8.5, 12.3, 9.2, 6.8],
'市净率': [12.3, 8.9, 0.9, 1.5, 1.1, 0.7],
'ROE': [0.28, 0.22, 0.15, 0.18, 0.13, 0.14],
'股息率': [0.012, 0.018, 0.035, 0.028, 0.040, 0.045]
})
print('行业数据:')
print(industry_data)
# 按行业分组计算均值
industry_mean = industry_data.groupby('行业').mean(numeric_only=True)
print('\n行业均值:')
print(industry_mean)
# 按行业分组计算多个统计量
industry_stats = industry_data.groupby('行业').agg({
'市盈率': ['mean', 'median', 'std'],
'市净率': ['mean', 'min', 'max'],
'ROE': 'mean',
'股息率': 'mean'
})
print('\n行业详细统计:')
print(industry_stats.round(4))
# 分组计数
industry_count = industry_data.groupby('行业').size()
print('\n行业股票数量:')
print(industry_count)54.8.3 多级分组
# 创建多级数据
multi_level_data = pd.DataFrame({
'行业': ['白酒', '白酒', '白酒', '银行', '银行', '银行', '保险', '保险'],
'市值等级': ['大盘', '中盘', '大盘', '大盘', '小盘', '大盘', '大盘', '中盘'],
'市盈率': [45.2, 35.8, 38.5, 8.5, 15.2, 6.8, 12.3, 18.5],
'收益率': [0.05, 0.03, 0.06, 0.02, 0.04, 0.01, 0.03, 0.02]
})
df_multi = pd.DataFrame(multi_level_data)
print('多级分组统计:')
# 多级分组
multi_stats = df_multi.groupby(['行业', '市值等级']).agg({
'市盈率': 'mean',
'收益率': 'mean'
})
print(multi_stats)
# 按行业统计,并排名
df_multi['行业内PE排名'] = df_multi.groupby('行业')['市盈率'].rank()
print('\n行业内PE排名:')
print(df_multi)54.8.4 自定义聚合函数
# 定义自定义函数:计算市净率与市盈率的比率均值
def price_to_book_ratio(group):
'''计算市净率/市盈率比率'''
return (group['市净率'] / group['市盈率']).mean() # 按组计算PB/PE均值
# 定义自定义函数:基于ROE计算类夏普比率
def roe_adjusted_ratio(group, benchmark_roe=0.10):
'''计算ROE超额收益与波动的比率'''
excess_roe = group['ROE'].mean() - benchmark_roe # 超额ROE
return excess_roe / group['ROE'].std() if group['ROE'].std() > 0 else 0 # 类夏普比率
# 应用自定义函数
print('自定义聚合函数:') # 输出标题
print('\n市净率/市盈率比率:') # 输出PB/PE比率标题
print(industry_data.groupby('行业').apply(price_to_book_ratio)) # 按行业计算PB/PE比率
# 使用agg配合lambda计算市盈率范围
print('\n市盈率范围(最大-最小):') # 输出PE范围标题
print(industry_data.groupby('行业')['市盈率'].agg(lambda x: x.max() - x.min())) # 计算各行业PE极差54.9 相关性分析
54.9.1 协方差与相关系数
协方差(Covariance):
\[ \text{Cov}(X, Y) = E[(X - \mu_X)(Y - \mu_Y)] = \frac{1}{n-1}\sum_{i=1}^n (x_i - \bar{x})(y_i - \bar{y}) \]
相关系数(Correlation Coefficient):
\[ \rho_{X,Y} = \frac{\text{Cov}(X, Y)}{\sigma_X \sigma_Y} = \frac{\sum(x_i - \bar{x})(y_i - \bar{y})}{\sqrt{\sum(x_i - \bar{x})^2}\sqrt{\sum(y_i - \bar{y})^2}} \]
54.9.2 相关性计算
# 使用之前的收益率数据
df_returns_sample = df_returns
# 计算协方差矩阵
cov_matrix = df_returns_sample.cov()
print('协方差矩阵:')
print(cov_matrix)
# 计算相关系数矩阵
corr_matrix = df_returns_sample.corr()
print('\n相关系数矩阵:')
print(corr_matrix)
# 找出相关性最高的股票对
corr_unstack = corr_matrix.unstack()
corr_unstack = corr_unstack[corr_unstack != 1] # 排除自相关
top_corr = corr_unstack.abs().sort_values(ascending=False).head(5)
print('\n相关性最高的股票对:')
print(top_corr)相关性的金融意义:
| 相关系数 | 关系 | 金融含义 | 投资策略 |
|---|---|---|---|
| \(\rho \approx 1\) | 强正相关 | 同涨同跌 | 分散化效果差 |
| \(\rho \approx 0\) | 无相关 | 独立变动 | 有效分散化 |
| \(\rho \approx -1\) | 强负相关 | 此消彼长 | 对冲工具 |
现代投资组合理论(MPT):
\[ \sigma_p^2 = \sum_{i=1}^n \sum_{j=1}^n w_i w_j \sigma_i \sigma_j \rho_{ij} \]
当 \(\rho_{ij} < 1\) 时,组合方差小于各资产方差的加权平均,实现风险分散。
54.10 本章小结
要点:
- 描述性统计三层次:集中趋势(均值、中位数、众数)、离散程度(方差、标准差、极差)、分布形状(偏度、峰度);
describe()一次给出多数指标 - 口径差异:Pandas 的
var()/std()默认样本口径(ddof=1),NumPy 默认总体口径(ddof=0),跨库对比时必须统一 - 累积统计量:
cumsum()累加、cumprod()累乘;pct_change()转为收益率,(1+r).cumprod()得到净值曲线 - 窗口统计:
rolling(w)固定窗口(如 20 日均线、滚动波动率),expanding()从起点到当前的扩展窗口;两者输出前 w-1 个位置为 NaN 属正常 groupby()按”拆分-应用-合并”完成分组统计,agg支持多函数与自定义函数;corr()/cov()给出多变量的相关与协方差矩阵,是组合构建的输入
易错点:
- 偏度看尾、峰度看峰:正偏不代表”涨得多”,而是右尾更长;高峰度意味着极端值比正态假设下更常见
rolling(w).mean()的前 w-1 个值是 NaN,直接对其求均值或画图会看到起始段缺口,不是数据错误- 日频波动率年化要乘 \(\sqrt{252}\),收益率年化通常乘 252,两者口径混用会得到荒谬结果
groupby().apply()里的自定义函数若返回标量,结果是按组索引的 Series;返回结构不一致时结果形态难以预料- 协方差受量纲影响,比较变量间线性关系强弱应看相关系数
54.11 动手与思考
以下练习每题附参考答案(默认折叠)。请先独立完成并写下你的判断,再点开对照,最后上机验证。
输出预测:不运行代码,先写出下面代码的输出结果(注意第一个值),再上机检验你的判断。
import pandas as pd s = pd.Series([1, 2, 3, 4]) print(s.rolling(2).mean().tolist()) print(s.cumsum().tolist()) print(s.rolling(2).mean().isna().sum())参考答案(先写下你的预测再点开)
解题思路:
rolling(2).mean()是窗口为 2 的滚动均值——第 1 个位置前面只有 1 个观测、窗口不满,输出 NaN;之后依次是 (1+2)/2、(2+3)/2、(3+4)/2,即 [nan, 1.5, 2.5, 3.5]。cumsum()从第 1 个值起逐项累加,[1, 3, 6, 10]。滚动序列只有开头 1 个 NaN,计数为 1(一般地,窗口 w 的前 w−1 个位置为 NaN)。# 验证脚本:滚动均值、累加与起始段缺失计数 import pandas as pd # 导入pandas库 s = pd.Series([1, 2, 3, 4]) # 简单序列 print(s.rolling(2).mean().tolist()) # 窗口2滚动均值,首值窗口不满为NaN print(s.cumsum().tolist()) # 累计和 print(s.rolling(2).mean().isna().sum()) # 起始段NaN个数预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
[nan, 1.5, 2.5, 3.5] [1, 3, 6, 10] 1回扣主线:窗口统计前 w−1 个位置为 NaN 属正常,见第 章节 6 章统计运算部分与本章”要点”第 4 条。
自测回忆:不看正文,写出
describe()输出包含的统计量名称;再说明rolling(20).std()与expanding().std()在窗口范围上的差异,以及前 19 个值为什么是 NaN。参考答案(点开前请先独立完成)
解题思路:
describe()默认输出 8 个统计量:count(非缺失计数)、mean、std、min、25%、50%(中位数)、75%、max——即计数、集中趋势、离散程度与五个分位数。窗口差异:rolling(20).std()每次只用”最近 20 个”观测,窗口固定长度、随时间滑动,反映局部(近期)波动;expanding().std()的窗口从序列起点一直扩展到当前,用”截至当前的全部历史”,反映累积的长期波动。前 19 个值为 NaN 的原因:第 t 个位置(t 小于 19)之前不足 20 个观测,窗口不满无法计算 20 期标准差——这不是数据错误,而是滚动窗口的定义使然。回扣主线:滚动窗口与扩展窗口的对照见本章”窗口统计量”“expanding窗口”两节,运算基础见第 章节 6 章。
变式任务(平台任务同型改造):平台任务一把基金净值除以首日净值做归一化处理;请改用
pct_change()先算出日收益率,再用(1 + 收益率).cumprod()重建净值曲线,并与直接归一化的结果对照是否一致。参考答案(点开前请先独立完成)
解题思路:结论——一致(到浮点舍入精度)。理由:归一化是逐日净值除以首日净值 \(p_t/p_0\);而 \(\prod_{i=1}^{t}(1+r_i) = \prod_{i=1}^{t}(p_i/p_{i-1})\),连乘分子分母逐项相消(telescoping)后恰好等于 \(p_t/p_0\)。唯一差别是重建曲线的首行为 NaN——
pct_change()的第一个值没有前值。数据为 4 只 QDII 基金 117 个交易日的净值(OSS 直链实拉,拉取日期 2026-08-27)。# 变式脚本:两条净值重建路径的对照 import pandas as pd # 导入pandas库 value_QDII = pd.read_excel('https://huoran.oss-cn-shenzhen.aliyuncs.com/1726648479386.xlsx') # 读取QDII基金净值 value_QDII['日期'] = pd.to_datetime(value_QDII['日期'], format='%Y%m%d') # 日期列转日期类型 value_QDII.set_index('日期', inplace=True) # 日期设为行索引 value_QDII = value_QDII.dropna() # 删除缺失行 normalized = value_QDII / value_QDII.iloc[0] # 路径一:除以首日净值归一化 rebuilt = (1 + value_QDII.pct_change()).cumprod() # 路径二:日收益率累乘重建 print(normalized.iloc[:3, 0].round(6).tolist()) # 归一化曲线前3个值 print(rebuilt.iloc[:3, 0].round(6).tolist()) # 重建曲线前3个值(首值为NaN) print(float((normalized - rebuilt).abs().max().max())) # 两曲线最大绝对差预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
[1.0, 1.000438, 1.009528] [nan, 1.000438, 1.009528] 1.7763568394002505e-15最大绝对差约 1.8e-15,属浮点舍入量级,两条路径逐点一致。若 OSS 直链失效,可到教学平台运行平台任务同款代码,或用任一净值/价格序列演练,代码不变。
注意:以上为变式代码;列表 54.1 的平台任务仍须按原始代码原样输入教学平台。
回扣主线:
pct_change()与(1+r).cumprod()的净值曲线关系见本章”累积统计量”一节,与第 章节 6 章”切片错位相除”呼应。变式任务:构造一份含”行业”与”市盈率”两列的小数据框(至少 3 个行业、每个行业至少 3 条记录),用
groupby('行业')['市盈率'].agg(['mean', 'std', 'count'])输出分组统计表,并说明count一列帮助你确认了什么。参考答案(点开前请先独立完成)
解题思路:
groupby按”拆分—应用—合并”逐组计算,agg一次输出多个统计量。count一列确认两件事:一是每组的有效样本量(各组都是 3 条、无缺失),二是组间可比性——均值与标准差的比较必须建立在样本量与缺失情况已知的前提下,只有 3 个观测的组其 std 本身抽样误差就不小,跨组结论要谨慎。# 变式脚本:三行业的市盈率分组统计 import pandas as pd # 导入pandas库 pe_data = pd.DataFrame({ # 构造行业与市盈率小样本 '行业': ['银行', '银行', '银行', '白酒', '白酒', '白酒', '医药', '医药', '医药'], # 3个行业各3条 '市盈率': [5.2, 5.8, 6.1, 28.0, 32.5, 30.2, 35.1, 42.0, 38.7]}) # 市盈率取值 print(pe_data.groupby('行业')['市盈率'].agg(['mean', 'std', 'count']).round(4)) # 分组多统计量预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
mean std count 行业 医药 38.6000 3.4511 3 白酒 30.2333 2.2502 3 银行 5.7000 0.4583 3回扣主线:
groupby().agg()的拆分—应用—合并机制见本章”分组统计”一节,与第 章节 6 章按axis聚合的基础相衔接。思考题:金融收益率常呈”尖峰厚尾”。若风险管理系统假设收益服从正态分布,会系统性低估还是高估极端损失的概率?请结合峰度的含义说明。
参考答案(点开前请先独立完成)
解题思路:本题与第 章节 33 章第 5 题相同,可先做后对照。结论:会系统性低估极端损失的概率。峰度的含义是分布尾部相对于正态分布的厚度:高峰度意味着极端值(大幅亏损与大幅盈利)出现的频率高于正态假设下的预期。若风险系统按正态分布刻画收益,尾部概率被压得过小——同样置信水平(如 99%)下算出的分位数偏小,超过阈值的实际频率远高于模型给出的 1%,极端损失被系统性低估。判断是否厚尾可依据:偏度与峰度(超额峰度显著大于 0)、Q-Q 图两端是否翘起、样本极端分位数与正态理论分位数的对比。对风险度量的影响:应放弃正态假设下的参数化口径,改用历史分位数法、历史模拟 VaR,或对尾部建模的极值理论(EVT)等更稳健的做法,并辅以压力测试。
回扣主线:厚尾形态的诊断与”正态假设低估极端风险”见第 章节 33 章;偏度峰度的计算基础见本章”偏度与峰度”一节。